﻿using Google.Protobuf;
using System;
using System.ComponentModel;
using System.Net;
using System.Net.Sockets;
using System.Threading;

namespace DotNetClient
{
    /// <summary>
    /// network state enum
    /// </summary>
    public enum NetWorkState
    {
        [Description("initial state")]
        CLOSED,

        [Description("connecting server")]
        CONNECTING,

        [Description("server connected")]
        CONNECTED,

        [Description("disconnected with server")]
        DISCONNECTED,

        [Description("connect timeout")]
        TIMEOUT,

        [Description("netwrok error")]
        ERROR
    }

    public class SocketClient
    {
        public SocketClient() { }

        /// <summary>
        /// netwrok changed event
        /// </summary>
        public event Action<NetWorkState> NetWorkStateChangedEvent;

        private NetWorkState _netWorkState = NetWorkState.CLOSED; //current network state

        private EventManager _eventManager;
        private Socket _socket;
        private Protocol _protocol;
        private bool _disposed = false;
        private byte _requestId = 1;

        private ManualResetEvent _timeoutEvent = new ManualResetEvent(false);
        private int timeoutMSec = 8000; // connect timeout count in millisecond

        /// <summary>
        /// initialize client
        /// </summary>
        /// <param name="host">server name or server ip (www.xxx.com/127.0.0.1/::1/localhost etc.)</param>
        /// <param name="port">server port</param>
        /// <param name="callback">socket successfully connected callback(in network thread)</param>
        public void InitClient(string host, int port, Action callback = null)
        {
            _timeoutEvent.Reset();
            _eventManager = new EventManager();
            NetWorkChanged(NetWorkState.CONNECTING);

            IPAddress ipAddress = null;
            try
            {
                IPAddress[] addresses = Dns.GetHostEntry(host).AddressList;
                foreach (var item in addresses)
                {
                    if (item.AddressFamily == AddressFamily.InterNetwork)
                    {
                        ipAddress = item;
                        break;
                    }
                }
            }
            catch (Exception)
            {
                NetWorkChanged(NetWorkState.ERROR);
                return;
            }

            if (ipAddress == null)
            {
                throw new Exception("can not parse host : " + host);
            }

            _socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
            IPEndPoint ie = new IPEndPoint(ipAddress, port);

            _socket.BeginConnect(ie, new AsyncCallback((result) =>
            {
                try
                {
                    _socket.EndConnect(result);
                    _protocol = new Protocol(this, _socket);
                    NetWorkChanged(NetWorkState.CONNECTED);

                    callback?.Invoke();
                }
                catch (SocketException)
                {
                    if (_netWorkState != NetWorkState.TIMEOUT)
                    {
                        NetWorkChanged(NetWorkState.ERROR);
                    }
                    Dispose();
                }
                finally
                {
                    _timeoutEvent.Set();
                }
            }), _socket);

            if (_timeoutEvent.WaitOne(timeoutMSec, false))
            {
                if (_netWorkState != NetWorkState.CONNECTED && _netWorkState != NetWorkState.ERROR)
                {
                    NetWorkChanged(NetWorkState.TIMEOUT);
                    Dispose();
                }
            }
        }

        /// <summary>
        /// 网络状态变化
        /// </summary>
        /// <param name="state"></param>
        private void NetWorkChanged(NetWorkState state)
        {
            _netWorkState = state;
            NetWorkStateChangedEvent?.Invoke(state);
        }

        //-------------------------------------------------------------------------------
        //                              connect
        //-------------------------------------------------------------------------------
        public void Connect()
        {
            Connect(null);
        }

        public bool Connect(Action handshakeCallback)
        {
            try
            {
                _protocol.start(handshakeCallback);
                return true;
            }
            catch (Exception e)
            {
                Console.WriteLine(e.ToString());
                return false;
            }
        }

        //-------------------------------------------------------------------------------
        //                              request
        //-------------------------------------------------------------------------------
        internal void Request(string route, Action<Message> action)
        {
            Request(route, null, action);
        }

        internal void Request(string route, IMessage msg, Action<Message> action)
        {
            _eventManager.AddCallBack(_requestId, action);
            _protocol.send(route, _requestId, msg);

            idAutoIncrement();
        }

        private void idAutoIncrement()
        {
            if (_requestId >= 255) _requestId = 1;
            else _requestId++;
        }

        //-------------------------------------------------------------------------------
        //                              notify
        //-------------------------------------------------------------------------------
        internal void Notify(string route, IMessage msg)
        {
            _protocol.send(route, msg);
        }

        //-------------------------------------------------------------------------------
        //                              receive broadcast
        //-------------------------------------------------------------------------------
        internal void On(string eventName, Action<Message> action)
        {
            _eventManager.AddOnEvent(eventName, action);
        }

        //-------------------------------------------------------------------------------
        //                              internal
        //-------------------------------------------------------------------------------
        internal void ProcessMessage(Message msg)
        {
            if (msg.type == MessageType.MSG_RESPONSE)
            {
                _eventManager.InvokeCallBack(msg.id, msg);
            }
            else if (msg.type == MessageType.MSG_PUSH)
            {
                _eventManager.InvokeOnEvent(msg.route, msg);
            }
        }

        //-------------------------------------------------------------------------------
        //                              disconnect
        //-------------------------------------------------------------------------------
        public void Disconnect()
        {
            Dispose();
            NetWorkChanged(NetWorkState.DISCONNECTED);
        }

        public void Dispose()
        {
            Dispose(true);
            GC.SuppressFinalize(this);
        }

        // The bulk of the clean-up code
        protected virtual void Dispose(bool disposing)
        {
            if (_disposed) return;

            if (disposing)
            {
                // free managed resources
                _protocol?.close();
                _eventManager?.Dispose();

                try
                {
                    _socket.Shutdown(SocketShutdown.Both);
                    _socket.Close();
                    _socket = null;
                }
                catch (Exception)
                {
                    //todo : 有待确定这里是否会出现异常，这里是参考之前官方github上pull request。emptyMsg
                }

                _disposed = true;
            }
        }
    }
}
